#include "graphics/utils.h"
#include "graphics/image.h"
#include "graphics/framebuffer_object.h"
#include "graphics/program_glsl.h"
#include "graphics/cube_texture_gl.h"
#include "graphics/image_texture_gl.h"
#include "graphics/error_gl.h"
#include "tools/obj_loader.h"
#include "graphics/shader_object.h"
#include "shapes/triangle_mesh.h"
#include "silhouette_demo.h"
#include "nvModel/nvUtils.h"

namespace venus {
SilhouetteDemo::SilhouetteDemo() 
    : GlutWindow("Silhouette Demo",10, 10, 800, 600,  GLUT_RGBA|GLUT_DOUBLE|GLUT_DEPTH),
      camera_(Point(5, 5, 5), Point(0, 0, 0)) {
  camera_.set_fovy(40.0);
  camera_.set_z_far(1000.0);
  camera_.set_rotation_speed(10.f);
  camera_.set_velocity(0.5, 0.5, 0.5);
  Utils::SetNULL(model_);
  Utils::SetNULL(depth_tex_);
  Utils::SetNULL(fbo_);
}
SilhouetteDemo::~SilhouetteDemo() {
  Utils::Delete(model_);
  Utils::Delete(depth_tex_);
  Utils::Delete(fbo_);
}
bool SilhouetteDemo::Initialize() {
  glewInit();
  if (!glewIsSupported( "GL_VERSION_2_0 GL_EXT_framebuffer_object")) {
    printf( "Required extensions not supported.\n");
    return false;
  }
  model_ = new nv::Model;
  model_->loadModelFromFile("../models/cow.obj");
  model_->removeDegeneratePrims();
  model_->computeNormals();
  model_->compileModel(nv::Model::eptTrianglesWithAdjacency);

  this->LoadShaders();
  this->IntializeResources();
  render_method_ = 0;
  return true;
}

void SilhouetteDemo::OnDisplay() {
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  camera_.ApplyViewTransform();
  this->UpdateDepthMap();
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  // the depth value may be big, so that it may be difficult to recognize
  if (render_method_ == 1) VisualizeTexture(*depth_tex_);
  else this->RenderScene();
  DisplayStastics("frame rate", fps_counter_.Update(), "fps");
  glutSwapBuffers();
}

void SilhouetteDemo::OnResize(int width, int height) {
  if (width == 0 || height == 0) return;
  if (width != this->width() || height != this->height()) {
    this->set_width(width);
    this->set_height(height);
    this->ReleaseResources();
    this->IntializeResources();
  }
  glViewport(0, 0, width, height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  camera_.set_aspect_ratio(static_cast<double>(width)/height);
  camera_.ApplyProjectionTransform(); 
}
void SilhouetteDemo::OnIdle() {
  this->ReDraw();
}
void SilhouetteDemo::OnKeyboard(unsigned char key, int x, int y) {
  switch (key) {
    case 'a':
      camera_.Yaw(-1);
      break;
    case 'd':
      camera_.Yaw(1);
      break;
    case 'w':
      camera_.Walk(1);
      break;
    case 's':
      camera_.Walk(-1);
      break;
    case 'q':
      camera_.Pitch(1);
      break;
    case 'z':
      camera_.Pitch(-1);
      break;
    case 'n':
      render_method_ = 1 - render_method_;
      break;
  }
}

void SilhouetteDemo::OnSpecialKey(int key, int x, int y) {
  switch(key) {
  case GLUT_KEY_UP:
    camera_.Fly(1);
    break;
  case GLUT_KEY_DOWN:
    camera_.Fly(-1);
    break;
  case GLUT_KEY_LEFT:
    camera_.Strafe(-1);
    break;
  case GLUT_KEY_RIGHT:
    camera_.Strafe(1);
    break;
  default:
    break;
  }
}

void SilhouetteDemo::DrawModel() {
  glPushMatrix();
  glScalef(0.3, 0.3, 0.3);
  nv::DrawWithAdjacency(model_);
  glPopMatrix();
}
void SilhouetteDemo::RenderScene() {
  depth_tex_->Bind();
  glColor3f(1, 0, 0);
  silhouette_program_->Begin();
  //glUniform1i(depth_tex_var_, 0);
  this->DrawModel();
  silhouette_program_->End();
}

void SilhouetteDemo::ReleaseResources() {
  Utils::Delete(depth_tex_);
  Utils::Delete(fbo_);
}
void SilhouetteDemo::IntializeResources() {
  depth_tex_ = new ImageTextureGL;
  depth_tex_->Initialize(this->width(), this->height(), 
    GL_DEPTH_COMPONENT, 
    kDepthTexture|kUseDefaultParameters);
  fbo_ = new FramebufferObject;
  fbo_->Bind();
  fbo_->AttachTexture(depth_tex_->target(), depth_tex_->tex_id(), 
    GL_DEPTH_ATTACHMENT_EXT);
  glDrawBuffer(GL_NONE);
  glReadBuffer(GL_NONE);
  fbo_->IsValid();
  fbo_->Disable();
}

void SilhouetteDemo::LoadShaders() {
  silhouette_program_ = new ProgramGLSL;
  silhouette_program_->Load("shaders/silhouette.vert.glsl",
    "shaders/silhouette.frag.glsl");
  ShaderObject geom_shader(GL_GEOMETRY_SHADER_EXT, 
    "shaders/silhouette.geom.glsl");
  silhouette_program_->AttachShader(geom_shader);

  GLuint pp = silhouette_program_->program_id();
  glProgramParameteriEXT(pp,GL_GEOMETRY_INPUT_TYPE_EXT,GL_TRIANGLES_ADJACENCY_EXT);
  glProgramParameteriEXT(pp,GL_GEOMETRY_OUTPUT_TYPE_EXT, GL_LINE_STRIP );

  //the following is mandatory!!!
  int temp;
  glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES_EXT,&temp);
  glProgramParameteriEXT(pp,GL_GEOMETRY_VERTICES_OUT_EXT,temp);
  silhouette_program_->Link();
  depth_tex_var_ = silhouette_program_->GetUniformVar("depth_tex");
  //if don't call the Begin(), there'll be an error for glUniform1i(..)
  silhouette_program_->Begin();
  glUniform1i(depth_tex_var_, 0);
  silhouette_program_->End();
  CheckErrorsGL("LoadShaders");
}

void SilhouetteDemo::UpdateDepthMap() {
  glEnable(GL_POLYGON_OFFSET_FILL);
  glEnable(GL_DEPTH_TEST);
  fbo_->Bind();
  fbo_->IsValid();
  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  glDepthMask(GL_TRUE);
  //glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); 
  this->DrawModel();
  fbo_->Disable();
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_POLYGON_OFFSET_FILL);
  //glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
  //glDepthMask(GL_FALSE);
}

} // namespace venus
